/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Xceed.Silverlight.Data
{
  internal class OneWayEnumerator<T> : AsyncParentEnumerator<T>
  {
    #region Static Fields

    private static readonly Guid JumpMoveAndFetchCallId = DataUtils.CreateCallId();
    private static readonly Guid GetOneWayActionCallId = DataUtils.CreateCallId();
    private static readonly Guid GetTwoWayActionCallId = DataUtils.CreateCallId();
    private static readonly Guid GetTwoWayActionReversedCallId = DataUtils.CreateCallId();

    #endregion Static Fields


    internal OneWayEnumerator( IAsyncEnumerator childEnumerator )
      : base( childEnumerator )
    {
    }


    public override JumpMoveAndFetchAsyncResult BeginJumpMoveAndFetch(
      JumpMoveAndFetchParameters parameters,
      AsyncCallback callback,
      object state )
    {
      this.CheckMoveAndFetchParameters( parameters.ExpectedMoveCount, parameters.ExpectedFetchCount );

      JumpMoveAndFetchAsyncResult asyncResult = new JumpMoveAndFetchAsyncResult(
                                                  OneWayEnumerator<T>.JumpMoveAndFetchCallId,
                                                  callback,
                                                  state,
                                                  parameters );

      if( !this.HandleOneWayAction( asyncResult, parameters ) )
        if( !this.HandleTwoWayAction( asyncResult, parameters ) )
          throw new DataInternalException();

      return asyncResult;
    }

    public override JumpMoveAndFetchResult<T> EndJumpMoveAndFetch(
      JumpMoveAndFetchAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( OneWayEnumerator<T>.JumpMoveAndFetchCallId );

      return asyncResult.GetCallResult<JumpMoveAndFetchResult<T>>();
    }

    protected override void Dispose( bool disposing )
    {
      m_disposed = true;

      base.Dispose( disposing );
    }

    private bool HandleOneWayAction(
      JumpMoveAndFetchAsyncResult asyncResult,
      JumpMoveAndFetchParameters parameters )
    {
      long moveCount = parameters.ExpectedMoveCount;
      int fetchCount = parameters.ExpectedFetchCount;

      // Make sure the query is unidirectional.
      if( ( moveCount != 0 )
        && ( fetchCount != 0 )
        && ( DataUtils.IsQueryingForward( moveCount, fetchCount ) != DataUtils.IsQueryingForward( 0, fetchCount ) ) )
        return false;

      Func<AsyncCallback, object, IAsyncResult> begin =
        ( cb, st ) => this.BeginJumpMoveAndFetchOneWay( parameters, cb, st );
      Func<AsyncResult, IAsyncResult, DependentCallResult> end =
        ( par, iar ) => this.EndJumpMoveAndFetchOneWay( ( JumpMoveAndFetchAsyncResult )iar );

      asyncResult.Start( begin, end );

      return true;
    }

    private bool HandleTwoWayAction(
      JumpMoveAndFetchAsyncResult asyncResult,
      JumpMoveAndFetchParameters parameters )
    {
      long moveCount = parameters.ExpectedMoveCount;
      int fetchCount = parameters.ExpectedFetchCount;

      // Make sure the query is bidirectional.
      if( ( moveCount == 0 )
        || ( fetchCount == 0 )
        || ( DataUtils.IsQueryingForward( moveCount, fetchCount ) == DataUtils.IsQueryingForward( 0, fetchCount ) ) )
        return false;

      // Try to apply an optimized version of a bidirectional query.
      if( this.HandleTwoWayActionReversed( asyncResult, parameters ) )
        return true;

      Func<AsyncCallback, object, IAsyncResult> getPages =
        ( cb, st ) => this.BeginJumpMoveAndFetchTwoWays( parameters, cb, st );
      Func<AsyncResult, IAsyncResult, DependentCallResult> mergePages =
        ( par, iar ) => this.EndJumpMoveAndFetchTwoWays( ( JumpMoveAndFetchAsyncResult )iar );

      asyncResult.Start( getPages, mergePages );

      return true;
    }

    private bool HandleTwoWayActionReversed(
      JumpMoveAndFetchAsyncResult asyncResult,
      JumpMoveAndFetchParameters parameters )
    {
      long moveCount = parameters.ExpectedMoveCount;
      int fetchCount = parameters.ExpectedFetchCount;

      // Make sure the query is bidirectional, but can be emulated as unidirectional.
      if( ( moveCount == 0 )
        || ( fetchCount == 0 )
        || ( DataUtils.IsQueryingForward( moveCount, fetchCount ) == DataUtils.IsQueryingForward( 0, fetchCount ) ) )
        return false;

      // A query that starts out of bound can be optimized into an unidirectional query.
      SourcePosition sourcePosition = parameters.SourcePosition;
      if( !sourcePosition.IsOutOfBound )
      {
        long move = Math.Abs( moveCount );
        int fetch = Math.Abs( fetchCount );

        // There is no way to optimized the query since it needs to fetch items on both
        // sides of the starting position.
        if( move < fetch - 1 )
          return false;

        // A SortPosition may swallow a move to get on the first item.  We have to consider
        // this special case it may not be detected by the previous test.
        if( ( move < fetch ) && ( sourcePosition is SortPosition ) )
          return false;
      }

      Func<AsyncCallback, object, IAsyncResult> getPages =
        ( cb, st ) => this.BeginJumpMoveAndFetchTwoWaysReversed( parameters, cb, st );
      Func<AsyncResult, IAsyncResult, DependentCallResult> mergePages =
        ( par, iar ) => this.EndJumpMoveAndFetchTwoWaysReversed( ( JumpMoveAndFetchAsyncResult )iar );

      asyncResult.Start( getPages, mergePages );

      return true;
    }

    private JumpMoveAndFetchAsyncResult BeginJumpMoveAndFetchOneWay(
      JumpMoveAndFetchParameters parameters,
      AsyncCallback callback,
      object state )
    {
      long moveCount = parameters.ExpectedMoveCount;
      int fetchCount = parameters.ExpectedFetchCount;

      // Make sure the query is unidirectional.
      Debug.Assert( ( moveCount == 0 )
                 || ( fetchCount == 0 )
                 || ( DataUtils.IsQueryingForward( moveCount, fetchCount ) == DataUtils.IsQueryingForward( 0, fetchCount ) ) );

      Func<AsyncCallback, object, IAsyncResult> begin =
        ( cb, st ) => base.BeginJumpMoveAndFetch( parameters, cb, st );
      Func<AsyncResult, IAsyncResult, DependentCallResult> end =
        ( par, iar ) => new DependentCallResult( base.EndJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )iar ) );

      JumpMoveAndFetchAsyncResult asyncResult = new JumpMoveAndFetchAsyncResult(
                                                  OneWayEnumerator<T>.GetOneWayActionCallId,
                                                  callback,
                                                  state,
                                                  parameters );

      asyncResult.Start( begin, end );

      return asyncResult;
    }

    private DependentCallResult EndJumpMoveAndFetchOneWay(
      JumpMoveAndFetchAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( OneWayEnumerator<T>.GetOneWayActionCallId );

      return new DependentCallResult( asyncResult.GetCallResult<JumpMoveAndFetchResult<T>>() );
    }

    private JumpMoveAndFetchAsyncResult BeginJumpMoveAndFetchTwoWays(
      JumpMoveAndFetchParameters parameters,
      AsyncCallback callback,
      object state )
    {
      long moveCount = parameters.ExpectedMoveCount;
      int fetchCount = parameters.ExpectedFetchCount;

      // Make sure the query is bidirectional.
      Debug.Assert( ( moveCount != 0 )
                 && ( fetchCount != 0 )
                 && ( DataUtils.IsQueryingForward( moveCount, fetchCount ) != DataUtils.IsQueryingForward( 0, fetchCount ) ) );

      // This anonymous method gets to the first desired item.
      Func<AsyncCallback, object, IAsyncResult> doMove =
        ( cb, st ) =>
        {
          JumpMoveAndFetchParameters newParameters = new JumpMoveAndFetchParameters(
                                                       parameters.SourcePosition,
                                                       moveCount,
                                                       0,
                                                       parameters.ApproximateMove );

          return this.BeginJumpMoveAndFetch( newParameters, cb, st );
        };

      // This anonymous method gets the desired items.
      Func<AsyncResult, IAsyncResult, DependentCallResult> doFetch =
        ( par, iar ) =>
        {
          JumpMoveAndFetchResult<T> moveResult = this.EndJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )iar );

          if( m_disposed )
            return new DependentCallResult( null );

          if( !moveResult.Result )
            return new DependentCallResult(
                     new JumpMoveAndFetchResult<T>(
                       ( JumpMoveAndFetchAsyncResult )par,
                       moveResult.ActualMoveCount,
                       moveResult.FetchedItems,
                       moveResult.Result,
                       moveResult.EndPositionObject,
                       moveResult.EndPositionOffset,
                       moveResult.TotalCount ) );

          Func<AsyncCallback, object, IAsyncResult> beginFetch =
            ( cb, st ) =>
            {
              JumpMoveAndFetchParameters newParameters = new JumpMoveAndFetchParameters(
                                                           moveResult.EndPositionObject,
                                                           0,
                                                           fetchCount,
                                                           parameters.ApproximateMove );

              return this.BeginJumpMoveAndFetch( newParameters, cb, st );
            };

          Func<AsyncResult, IAsyncResult, DependentCallResult> endFetch =
            ( p, ar ) =>
            {
              JumpMoveAndFetchResult<T> fetchResult = this.EndJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )ar );

              if( m_disposed )
                return new DependentCallResult( null );

              return new DependentCallResult(
                       new JumpMoveAndFetchResult<T>(
                         ( JumpMoveAndFetchAsyncResult )p,
                         moveResult.ActualMoveCount,
                         fetchResult.FetchedItems,
                         fetchResult.Result,
                         fetchResult.EndPositionObject,
                         fetchResult.EndPositionOffset,
                         fetchResult.TotalCount ) );
            };

          return new DependentCallResult( null, beginFetch, endFetch );
        };

      JumpMoveAndFetchAsyncResult asyncResult = new JumpMoveAndFetchAsyncResult(
                                                  OneWayEnumerator<T>.GetTwoWayActionCallId,
                                                  callback,
                                                  state,
                                                  parameters );

      asyncResult.Start( doMove, doFetch );

      return asyncResult;
    }

    private DependentCallResult EndJumpMoveAndFetchTwoWays(
      JumpMoveAndFetchAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( OneWayEnumerator<T>.GetTwoWayActionCallId );

      return new DependentCallResult( asyncResult.GetCallResult<JumpMoveAndFetchResult<T>>() );
    }

    private JumpMoveAndFetchAsyncResult BeginJumpMoveAndFetchTwoWaysReversed(
      JumpMoveAndFetchParameters parameters,
      AsyncCallback callback,
      object state )
    {
      long moveCount = parameters.ExpectedMoveCount;
      int fetchCount = parameters.ExpectedFetchCount;
      SourcePosition sourcePosition = parameters.SourcePosition;

      // Make sure the query is an optimizable bidirectional query.
      Debug.Assert( ( moveCount != 0 )
                 && ( fetchCount != 0 )
                 && ( DataUtils.IsQueryingForward( moveCount, fetchCount ) != DataUtils.IsQueryingForward( 0, fetchCount ) ) );
      Debug.Assert( ( sourcePosition.IsOutOfBound )
                 || ( Math.Abs( moveCount ) >= Math.Abs( fetchCount ) - 1 )
                 || ( ( Math.Abs( moveCount ) >= Math.Abs( fetchCount ) ) && ( sourcePosition is SortPosition ) ) );

      bool failFromStart = false;
      SourcePosition endPosition = null;
      RelativeOffset endPositionOffset = null;

      // Since we are starting out of bound, we already know that at least one item 
      // will be missing at the end.
      if( ( Math.Abs( moveCount ) <= Math.Abs( fetchCount ) - 1 )
        && ( sourcePosition.IsOutOfBound ) )
      {
        int moveUnit = DataUtils.GetUnit( moveCount );
        fetchCount = Convert.ToInt32( Math.Max( 1, Math.Abs( moveCount ) ) ) * moveUnit;
        moveCount = moveUnit;

        failFromStart = true;
        endPosition = sourcePosition;
        endPositionOffset = new RelativeOffset(
                              0,
                              ( sourcePosition.IsBeforeFirst )
                                ? ReferencePoint.BeforeFirst
                                : ReferencePoint.AfterLast );
      }
      // Transform the query into an unidirectional query.
      else
      {
        moveCount += fetchCount + DataUtils.GetUnit( moveCount );
        fetchCount = -fetchCount;
      }

      // This anonymous method starts the unidirectional query.
      Func<AsyncCallback, object, IAsyncResult> getData =
        ( cb, st ) =>
        {
          JumpMoveAndFetchParameters newParameters = new JumpMoveAndFetchParameters(
                                                       sourcePosition,
                                                       moveCount,
                                                       fetchCount,
                                                       parameters.ApproximateMove );

          return this.BeginJumpMoveAndFetch( newParameters, cb, st );
        };

      // This anonymous method reverse the result set.
      Func<AsyncResult, IAsyncResult, DependentCallResult> reverseData =
        ( par, iar ) =>
        {
          JumpMoveAndFetchResult<T> result = this.EndJumpMoveAndFetch( ( JumpMoveAndFetchAsyncResult )iar );

          if( m_disposed )
            return new DependentCallResult( null );

          bool success = !failFromStart && result.Result;
          Nullable<long> actualMoveCount = result.ActualMoveCount;
          ICollection<T> fetchedItems = result.FetchedItems;
          EstimateInt64 totalItemsCount = result.TotalCount;

          // We were able to move to the appropriate location.
          if( result.Result )
          {
            // Reverse the fetched items.
            List<T> currentItems = new List<T>( fetchedItems.Reverse() );
            fetchedItems = currentItems;

            // Find out the real actual move count.
            if( actualMoveCount.HasValue )
            {
              actualMoveCount = actualMoveCount.Value + result.GetFetchDisplacement();
            }

            if( !failFromStart )
            {
              endPosition = result.EndPositionObject;
              endPositionOffset = result.EndPositionOffset;

              // Find out the real end position.
              if( currentItems.Count > 1 )
              {
                endPosition = new ItemPosition( currentItems.Last() );

                if( endPositionOffset != null )
                {
                  long offset = ( currentItems.Count - 1 ) * -result.GetFetchUnit();

                  if( endPositionOffset.ReferencePosition == ReferencePoint.BeforeFirst )
                  {
                    endPositionOffset += offset;
                  }
                  else
                  {
                    endPositionOffset -= offset;
                  }
                }
              }
            }
          }
          // The move went beyond the end of the source.
          else
          {
            // Find out the real actual move count.
            if( actualMoveCount.HasValue )
            {
              JumpMoveAndFetchAsyncResult parentAsyncResult = ( JumpMoveAndFetchAsyncResult )par;
              actualMoveCount = actualMoveCount.Value + result.GetFetchDisplacement();
            }

            // Find out if we are before or after the end of source.
            if( result.ExpectedFetchCount > 0 )
            {
              endPosition = SourcePosition.AfterLast;
              endPositionOffset = new RelativeOffset( 0, ReferencePoint.AfterLast );
            }
            else
            {
              endPosition = SourcePosition.BeforeFirst;
              endPositionOffset = new RelativeOffset( 0, ReferencePoint.BeforeFirst );
            }

            // Since we went beyond the end of the source, the fetch part shouldn't have
            // occured.  We must not return any items.
            fetchedItems = null;
          }

          return new DependentCallResult(
                   new JumpMoveAndFetchResult<T>(
                     ( JumpMoveAndFetchAsyncResult )par,
                     actualMoveCount,
                     fetchedItems,
                     success,
                     endPosition,
                     endPositionOffset,
                     totalItemsCount ) );
        };

      JumpMoveAndFetchAsyncResult asyncResult = new JumpMoveAndFetchAsyncResult(
                                                  OneWayEnumerator<T>.GetTwoWayActionReversedCallId,
                                                  callback,
                                                  state,
                                                  parameters );

      asyncResult.Start( getData, reverseData );

      return asyncResult;
    }

    private DependentCallResult EndJumpMoveAndFetchTwoWaysReversed(
      JumpMoveAndFetchAsyncResult asyncResult )
    {
      asyncResult.ValidateCallEnd( OneWayEnumerator<T>.GetTwoWayActionReversedCallId );

      return new DependentCallResult( asyncResult.GetCallResult<JumpMoveAndFetchResult<T>>() );
    }


    #region Private Fields

    private bool m_disposed; //false

    #endregion
  }
}
